iT邦幫忙

2025 iThome 鐵人賽

DAY 7
2
Security

Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒!系列 第 7 篇

Day07 - 一窺窗戶中的風景:深入淺出 Windows API

  • 分享至 

  • xImage
  •  

走在時代前沿的前言

嗨大家我又來了,我 CX330。昨天我們已經初探了 Windows 的架構和記憶體分配等等的,今天要來跟大家聊聊 Windows API!

Windows API 是微軟提供給開發者讓其跟 Windows 作業系統互動的方式,比如如果應用程式需要在螢幕上顯示一個視窗、要修改檔案或是查詢註冊表(Registry),那就會需要用到 Windows API。

好啦那廢話不多說,蜥蜴軍團們,我們開始囉!

疊甲

中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」

本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!

Zig 版本

本系列文章中使用的 Zig 版本號為 0.14.1。

Windows 資料型別

Windows API 有很多除了 C 語言的基本資料型態(如 int、flaot 等)外的資料型別,這些都有被文檔紀錄,可以在這邊看到。以下會列出一些常見的資料型別:

  • DWORD
    • 一個 32 位元的無符號整數,在 32 和 64 位元系統上都存在
  • SIZE_T
    • 用於表示物件的大小,在 32 位元的系統上,它是一個 32 位的無符號整數;在 64 位元上則是 64 位的無符號整數
  • VOID
    • 表示不存在的資料型別
  • PVOID
    • 他是一個指向任何資料型別的指針。在 32 位元系統上大小為 32 位,64 位元系統上則為 64 位元。
  • HANDLE
    • 指定作業系統正在管理的特定物件的值,如檔案、進程、線程等
  • HMODULE
    • 是 Module 的 Handle 的意思,是該 Module 的基址。一個 Module 可能是一個 DLL 或是一個 EXE 等等
  • LPCSTR/PCSTR
    • 指向一個 8 位元的 ANSI 空終止字串(Null-terminated string)常數。L 代表 Long,這是起源於 16 位元的 Windows 程式設計時代,現在它不會影響資料型別,但這個命名的慣例被保存了下來。C 代表 Constant 表示常數。這兩種資料類型和 const char* 等效
  • LPSTR/PSTR
    • 和 LPCSTR/PCSTR 唯一的區別在於 LPSTR/PSTR 不是指向常數字串,而是指向可讀寫的字串,與 char* 等效
  • LPCWSTR/PCWSTR
    • 指向一個 16 位元的 Windows Unicode 空終止字串,與 const wchar* 等效
  • LPWSTR/PWSTR
    • 和 LPCWSTR/PCWSTR 唯一的區別在於 LPWSTR/PWSTR 不是指向常數字串,而是智想可讀寫的字串,與 wchar* 等效
  • wchar_t
    • 等同於 wchar,用來表示寬字符
  • ULONG_PTR
    • 表示一個和指定架構上的指針大小相同的無符號整數,代表 32 位元系統上它是 32 位大小,64 位系統上則是 64 位元。

指針資料型別

Windows API 允許開發人員可以直接宣告資料型別或是資料型別的指針,這會在資料型態的名稱中有所體現。以 "P" 開頭的資料型別代表著該資料型別的指針,而沒有 "P" 的代表該資料型別本身,例如:

  • PHANDLE 等於 HANDLE*
  • PSIZE_T 等於 SIZE_T*
  • PDWORD 等於 DWORD*

ANSI & Unicode 函數

在 Windows API 的很多函數中都會看到兩個不同的版本,分別會以 "A" 結尾或是 "W" 結尾。舉例來說,我們有 CreateFileA 和 CreateFileW。結尾為 "A" 的表示 ANSI,而結尾為 "W" 的表示 Unicode 或是 Wide。

身為開發人員(惡意程式開發人員也是開發人員),最重要的事情是要記住,ANSI 函數會接受 ANSI 資料型別作為參數,而 Unicode 函數則要接收 Unicode 型別。像是 CreateFileA 的第一個參數是 LPCSTR 而 CreateFileW 的第一個參數則為 LPCWSTR。

我們可以看一下以下的比較,來更清楚的知道兩者的區別。

char str1[] = "CX330";	  // 6 bytes (5 + null byte)
wchar str2[] = L"CX330";  // 12 bytes, each char is 2 bytes

入參與出參

在 Windows API 中有兩種參數,入參和出參。IN 參數是一個傳遞給函數的參數,用於作爲輸入;而 OUT 參數則用於將值回傳給函數的呼叫者(Caller)。出參通常會通過指針去傳遞引用(Reference)。

這邊舉個例子,有個叫作 ShareThisArticle 的函數(我亂取的,快按讚訂閱分享!),會接收一個整數的真並將其值設置為 123。這會被當作是一個出參,因為參數返回了一個新的值。

BOOL ShareThisArticle(OUT int* num) {
    *num = 123;
    return True;
}

int main() {
    int a = 0;
    ShareThisArticle(&a);
}

值得注意的是,這個 OUT 或是 IN 的關鍵字目的是為了讓開發者更好理解函數的期望參數和對參數的處理過程,加與不加並不會影響函數的運行和編譯,也不會影響該參數為入參或出參。

或許大家會覺得今天講的內容很無趣,不過理解這些命名慣例和資料型別等等,都對於日後在閱讀 Microsoft 提供的 Windows API 文檔很有幫助。

Zig 調用 Windows API

在本系列中,我們會使用兩種方式來調用 Windows API,分別是:

  1. 使用內建封裝好的模組
  2. 自己 extern 該函數

我們一個一個來看看,首先我們看一下第一種方式。在 Zig 語言的標準函式庫裡面有一個已經封裝好的一些常見的 Windows API 和 Windows 資料型別,在 std.os.windows 模組之中。比如說我們要獲取一個 CreateProcessW 函數,只需要透過以下的程式碼就可以了。

const std = @import("std");
const win = std.os.windows;

const CreateProcessW = win.CreateProcessW;

這樣一來,我們就可以調用 CreateProcessW 了!

那接著來看一下第二個方法,就是使用 extern 關鍵字來直接宣告目標 DLL 的 Windows API 函數原形。比如說我們要使用 CreateFileW 這個函數,就可以像這樣:

const std = @import("std");
const win = std.os.windows;

extern fn CreateFileW(
    lpFileName: [*:0]const u16,
    dwDesiredAccess: win.DWORD,
    dwShareMode: win.DWORD,
    lpSecurityAttributes: ?*win.SECURITY_ATTRIBUTES,
    dwCreationDisposition: win.DWORD,
    dwFlagsAndAttributes: win.DWORD,
    hTemplateFile: ?win.HANDLE,
) callconv(win.WINAPI) win.HANDLE;

在下一個章節中,我們會寫一個範例,用 Zig 來呼叫 CreateFileW 並建立文件。

Windows API 範例

我們來示範一下 CreateFileW 函數的使用。我們在不確定一個函數的功能的時候,請務必要去閱讀一下參考文件或是官方的文檔,以確保該函數的行為和我們的預期相同。CreateFileW 的文檔在這邊,我幫大家把文檔中的函數原形放過來。

HANDLE CreateFileW(
  [in]           LPCWSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);

閱讀後會發現,在文檔中提到如果函數成功執行,回傳值將會是對指定檔案、設備、命名管道或 Mail slot 的開啟的句柄(Handle),因此 CreateFileW 將會回傳 我們建立的物件的 HANDLE 資料型別。

此外,這個函數的所有參數都是 in 參數,代表函數不會從參數回傳任何資料。在這邊想提一下,Windows API 官方文檔中的方括號中的字,包括 in、out、optional 等都只是為了讓開發者參考,並沒有什麼實際的影響。

接著我們就用 Zig 語言來示範一下這個函數的用法,用於在我的當前目錄建立一個 Hi.txt。

const std = @import("std");
const win = std.os.windows;

extern fn CreateFileW(
    lpFileName: [*:0]const u16,
    dwDesiredAccess: win.DWORD,
    dwShareMode: win.DWORD,
    lpSecurityAttributes: ?*win.SECURITY_ATTRIBUTES,
    dwCreationDisposition: win.DWORD,
    dwFlagsAndAttributes: win.DWORD,
    hTemplateFile: ?win.HANDLE,
) callconv(win.WINAPI) win.HANDLE;

extern fn CloseHandle(
    hObject: win.HANDLE,
) callconv(win.WINAPI) win.BOOL;

pub fn main() !void {
    const path: [:0]const u16 = &[_:0]u16{
        'H', 'i', '.', 't', 'x', 't', 0,
    };

    const h = CreateFileW(
        path,
        win.GENERIC_WRITE,
        0,
        null,
        win.CREATE_ALWAYS,
        win.FILE_ATTRIBUTE_NORMAL,
        null,
    );
    if (h == win.INVALID_HANDLE_VALUE) return error.CreateFailed;

    _ = CloseHandle(h);
}

這邊的 main 函數中會先定義一個 path,是個空終止的 UTF-16 寬字串,每個字元為 16 位元。然後傳入給 CreateFileW 的參數如下:

  • path
    • 路徑
  • GENERIC_WRITE
    • 要求寫入權限
  • 0
    • 不共享,其他進程不能存取
  • null
    • 預設安全性
  • CREATE_ALWAYS
    • 無論如何都要創建,若存在則覆蓋
  • FILE_ATTRIBUTE_NORMAL
    • 一般屬性
  • null
    • 無範本句柄

之後這個 CreateFileW 會回傳一個文件句柄給 h,並會去檢查是否錯誤。最後再用 CloseHandle(h) 關掉句柄並把回傳值丟掉給 _。

Windows API 除錯

當函數執行失敗的時候,它們通常都會回傳很詳細的錯誤。像是如果 CreateFileW 執行失敗,它會回傳 INVALID_HANDLE_VALUE,表示無法建立檔案。要了解檔案無法建立的原因,我們可以使用 GetLastError 函數來取得錯誤碼。

取得錯誤碼後,需要查詢 Windows 的系統錯誤碼清單,以下為一些常見的錯誤碼。

  • 5
    • ERROR_ACCESS_DENIED
  • 2
    • ERROR_FILE_NOT_FOUND
  • 87
    • ERROR_INVALID_PARAMETER

Windows NTAPI 除錯

大家如果還記得昨天的內容,我們說過 NTAPIs 主要是從 ntdll.dll 匯出的。和普通的 Windows API 不同的是,它們不能透過 GetLastError 取得錯誤碼,相反,它們直接回傳錯誤,該錯誤碼會以 NTSTATUS 資料型別表示。

NTSTATUS 用來表示系統調用(System call)或是函數的狀態,並定義為 32 位無符號整數。成功的系統調用將返回 STATUS_SUCCESS,其值為 0。相反的,若是調用失敗,它將反為非 0 的值。這時候我們為了進一步知道錯誤的原因,我們可以把該 NTAPI 函數的回傳值打印出來,並檢查 Microsoft 對 NTSTATUS 的文檔。

另一種檢查回傳值的方式是透過這裡提到的 NT_SUCCESS 這個 Macro。這個 Macro 會在這個函數成功的時候回傳 TRUE,反之回傳 FALSE。

#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

這邊舉個 C 程式碼的例子,看一下如何使用這個 Macro。

NTSTATUS STATUS = ExampleNativeSyscall(...);
if (!NT_SUCCESS(STATUS)) {
    printf("ExampleNativeSyscall failed with status: 0x%08x\n", STATUS);
}

鐵人賽期 PoPoo,你今天轉 Po 了嗎?

結束囉!我要去吃晚餐了。今天帶大家了解了 Windows API 的各種資料型別和一些小知識點,也跟大家介紹了 Zig 如何使用 Windows API。明天應該會來詳細了解一下 PE 文件格式(這主題稍大,但是我明天超級無敵忙,救命,希望有時間可以寫完)。

如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!


上一篇
Day06 - 初探窗內的世界:Windows 架構與記憶體管理簡介
下一篇
Day08 - 惡意程式前哨戰:全面分解 PE 文件格式
系列文
Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒! 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
tt27
iT邦新手 5 級 ‧ 2025-09-21 20:30:31

好厲駭

我要留言

立即登入留言